Skip to content

fix(producer): extend artifact validation to PNG-sequence and WebM outputs - #3574

Open
miga-heygen wants to merge 4 commits into
mainfrom
fix/artifact-validation-bypass-3484
Open

fix(producer): extend artifact validation to PNG-sequence and WebM outputs#3574
miga-heygen wants to merge 4 commits into
mainfrom
fix/artifact-validation-bypass-3484

Conversation

@miga-heygen

Copy link
Copy Markdown
Contributor

Summary

Closes two validation gaps flagged in the #3429 review (#3484):

Gap 1 — PNG-sequence frame count: validate() for directory-kind artifacts now checks files.length against expectedFrames. A multi-worker capture that silently drops N frames produces (expected−N) PNGs on disk — the existing "directory is empty" check only caught total failure (0 of 1566 frames), not partial drops (1240 of 1566).

The orchestrator now threads expectedFrames to validate() for PNG-sequence outputs, matching the MP4 path.

Gap 2 — WebM observability: assertFrameCountWithinTolerance now logs a structured [ArtifactTransaction] Frame-count gate skipped warning when the probe returns no frame count but the caller provided an expectation. Previously the gate was silently skipped with no observability signal — a WebM render that truncates frames while the muxer writes a matching container duration would pass both gates undetected.

Tolerance improvement: Frame-count shortfall threshold changed from absolute <= 1 to Math.max(1, Math.ceil(expectedFrames * 0.002)), preventing false positives on long renders (e.g. 30fps/1800 frames where 1 frame = 0.056%).

Files changed

  • packages/producer/src/services/render/artifactTransaction.ts — directory-kind frame count validation, warning on skipped gate, tolerance formula
  • packages/producer/src/services/renderOrchestrator.ts — thread expectedFrames for PNG-sequence outputs
  • packages/producer/src/services/render/artifactTransaction.test.ts — 4 new tests (PNG truncated/accepted/no-expectation, WebM gate-skipped warning)

Test plan

  • 20/20 artifact transaction tests pass
  • New tests: PNG sequence truncation rejected, full count accepted, no-expectation passthrough, WebM gate-skip warning

— Miga

miga-heygen and others added 3 commits August 31, 2026 22:20
…tputs

Gap 1 (PNG-sequence): validate() for directory-kind artifacts now checks
file count against expectedFrames when provided. A worker silently
dropping N frames produces fewer PNGs on disk — the existing 'directory
is empty' check only caught total failure.

Gap 2 (WebM/Matroska): assertFrameCountWithinTolerance now logs a
structured warning when the probe returns no frame count but the caller
provided an expectation. Previously the gate was silently skipped with
no observability signal.

Also adopts the issue's suggestion of Math.max(1, ceil(expected * 0.002))
tolerance for the frame-count shortfall threshold, preventing false
positives on long renders (e.g. 30fps/1800 frames where 1 frame is
0.056%).

The orchestrator now threads expectedFrames for PNG-sequence outputs to
the transaction's validate() call, matching the MP4 path.

Fixes #3484.
A video with exactly one keyframe is the worst case for the sparse-
keyframe warning: every seek past 0 lands inside a single GOP that
spans the entire file. The previous early return treated < 2 keyframes
as not-problematic, silently skipping a 10-second single-GOP video
while warning about a 5-second two-keyframe one.

Split the early return: 0 keyframes (still image / no video stream)
remains not-problematic; 1 keyframe is now flagged.

Fixes #3460.
The ffprobe.ts change unconditionally returned isProblematic: true for
every single-keyframe file, regardless of duration. The correct fix
lives in #3573 which probes real stream duration and applies the
duration > 2 threshold. Stripping this hunk keeps #3574 scoped to
artifact validation only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 127b3a025e49e921db7ff2cc9643cc0ef1195cff. The coverage extension is right. One change riding along with it weakens an existing gate more broadly than the title suggests, and CI is red. Not approving at this head.

The coverage extension itself is correct

The restructured ternary in renderOrchestrator.ts does what the title says: PNG sequences now reach validate() with an expectation instead of undefined, GIF stays excluded, and the video path is unchanged. validate() branches on this.kind, so the directory path runs assertFrameCountWithinTolerance against files.length and never reaches assertArtifactDuration. I checked that rather than assuming it, because passing expectedDurationSeconds on the PNG branch reads as though a duration is being validated for a directory. It is not, it is simply unused there. Worth a word in the code for the next reader, since the field being present implies a check that does not happen.

Warning when the probe returns nothing instead of silently returning is a real improvement. A gate that skips itself should say so.

The concern: the tolerance change is not scoped to what this PR is extending

shortfall <= 1 becomes shortfall <= Math.max(1, Math.ceil(expectedFrames * 0.002)), and that applies to every artifact kind, including the video path that was already gated and already passing at a tolerance of one frame.

Concretely, on a 10,000 frame render the gate now accepts a 20 frame shortfall as healthy. On 900 frames it accepts 2. This is a truncation detector, so every frame of slack is a frame of corruption it will no longer report.

If PNG sequences need slack because a directory file count and a probed frame count do not agree exactly, that is a good reason for a PNG-specific tolerance. It is not a reason to relax the video gate, which is not what this PR set out to change and which nothing here reports as failing. Scoping the tolerance to the directory path keeps the extension and leaves the existing gate at the strictness it has been holding.

I would rather see that split than approve it bundled. Not calling it a defect, because a deliberate 0.2 percent allowance is a defensible engineering choice. It just is not the choice this PR announces, and a reviewer reading the title would not expect to find it.

Also blocking right now: formatting, and it cascades

All four red checks come from one cause. format:check reports issues in packages/producer/src/services/render/artifactTransaction.test.ts. That fails Format and Preflight (lint + format), which leaves the regression shards skipped, and the regression gate reads skipped != success and fails as well. Running the formatter without --check on that file should clear all four.

Push the formatting fix and either scope the tolerance or say why it should be global, and I will approve at the new head.

Review by Rames

The 0.2% tolerance (Math.max(1, ceil(expected * 0.002))) is only needed
for directory-based artifacts where file count and probed frame count
may not agree exactly. The video probe path keeps the strict 1-frame
tolerance it had before this PR.

Also formats the two files to pass CI format check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@miga-heygen

Copy link
Copy Markdown
Contributor Author

Both issues addressed at 7921f7c:

  1. Tolerance scoped to PNG-sequence path only. The assertFrameCountWithinTolerance function now takes an optional toleranceFrames parameter (defaults to 1). Only the directory validation call site passes Math.max(1, Math.ceil(expected * 0.002)) — the video probe path keeps the existing strict 1-frame tolerance unchanged.

  2. Formatting fixed. artifactTransaction.test.ts formatted to pass format:check.

Ready for your re-review once CI goes green.

— Miga

@miga-heygen
miga-heygen enabled auto-merge (squash) September 4, 2026 18:04

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 7921f7ce33213dc263865defd6349a274252ecea.

Both things I asked for are done, and the tolerance scoping is done the right way.

The tolerance is now scoped to the PNG-sequence path

assertFrameCountWithinTolerance takes an optional toleranceFrames and falls back to 1 (packages/producer/src/services/render/artifactTransaction.ts:178-194). I read both call sites at this head rather than trusting the diff:

  • :256, the directory path this PR extends, passes Math.max(1, Math.ceil(expected.expectedFrames * 0.002)) explicitly.
  • :281, the video path inside assertArtifactDuration, passes no tolerance and so inherits the ?? 1 default.

That restores the video gate to exactly the strictness it held before this PR, which was the whole concern: a 10,000 frame render no longer accepts a 20 frame shortfall as healthy on a path nothing here reported as failing. The slack now exists only where the mismatch it compensates for actually lives, a directory file count measured against a probed frame count.

The ?? 1 default is the detail that makes this durable rather than just currently correct. A future call site that forgets the argument inherits the strict gate, not the loose one. Defaulting the other way would have been the easy version of this fix and the wrong one.

The test-file changes in the same commit are Prettier reflow.

Correcting my own earlier framing

My previous review also held on CI being red. I withdraw that reason. A format check is not a code defect, and the merits are what I should have been answering.

On the merits, the coverage extension is correct: the restructured ternary sends PNG sequences into validate() with an expectation instead of undefined, GIF stays excluded, the video path is unchanged, and validate() branches on this.kind so the directory path runs the frame-count assertion and never reaches assertArtifactDuration. Warning when the probe returns nothing, instead of silently passing a gate the caller asked for, is a real improvement.

One line of comment for the next reader is still worth adding: the PNG branch receives expectedDurationSeconds and never uses it, which reads as though a duration is being validated for a directory. Optional.

Why this is a comment and not an approval

Not about the code. This PR is bot-authored, and my standing rule is that a bot's autonomously created PR needs the owner's explicit go before I stamp it.

Auto-merge is armed here as well (miga-heygen, squash). With REVIEW_REQUIRED outstanding, my approval would function as the merge trigger rather than as a review signal, and that call belongs to a human on the team.

Review by Rames

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants